msg_tool\scripts\hexen_haus\archive/
odio.rs1use crate::ext::io::*;
3use crate::scripts::base::*;
4use crate::types::*;
5use anyhow::{Result, anyhow};
6use std::io::{Read, Seek, SeekFrom};
7use std::sync::{Arc, Mutex};
8
9const ODIO_SIGNATURE: &[u8; 4] = b"ODIO";
10const HEADER_CHECK_OFFSET: u64 = 0x0A;
11const HEADER_CHECK_VALUE: u32 = 0xCCAE_01FF;
12const INDEX_START: u64 = 0x12;
13const INDEX_ENTRY_SIZE: u64 = 6;
14const ENTRY_HEADER_SIZE: u64 = 0x2C;
15
16#[derive(Debug)]
17pub struct HexenHausOdioArchiveBuilder;
19
20impl HexenHausOdioArchiveBuilder {
21 pub const fn new() -> Self {
23 HexenHausOdioArchiveBuilder
24 }
25}
26
27impl ScriptBuilder for HexenHausOdioArchiveBuilder {
28 fn default_encoding(&self) -> Encoding {
29 Encoding::Cp932
30 }
31
32 fn default_archive_encoding(&self) -> Option<Encoding> {
33 Some(Encoding::Cp932)
34 }
35
36 fn build_script(
37 &self,
38 buf: Vec<u8>,
39 _filename: &str,
40 _encoding: Encoding,
41 archive_encoding: Encoding,
42 config: &ExtraConfig,
43 _archive: Option<&Box<dyn Script>>,
44 ) -> Result<Box<dyn Script>> {
45 Ok(Box::new(HexenHausOdioArchive::new(
46 MemReader::new(buf),
47 archive_encoding,
48 config,
49 )?))
50 }
51
52 fn build_script_from_file(
53 &self,
54 filename: &str,
55 _encoding: Encoding,
56 archive_encoding: Encoding,
57 config: &ExtraConfig,
58 _archive: Option<&Box<dyn Script>>,
59 ) -> Result<Box<dyn Script>> {
60 if filename == "-" {
61 let data = crate::utils::files::read_file(filename)?;
62 return Ok(Box::new(HexenHausOdioArchive::new(
63 MemReader::new(data),
64 archive_encoding,
65 config,
66 )?));
67 }
68 let file = std::fs::File::open(filename)?;
69 let reader = std::io::BufReader::new(file);
70 Ok(Box::new(HexenHausOdioArchive::new(
71 reader,
72 archive_encoding,
73 config,
74 )?))
75 }
76
77 fn build_script_from_reader(
78 &self,
79 reader: Box<dyn ReadSeek>,
80 _filename: &str,
81 _encoding: Encoding,
82 archive_encoding: Encoding,
83 config: &ExtraConfig,
84 _archive: Option<&Box<dyn Script>>,
85 ) -> Result<Box<dyn Script>> {
86 Ok(Box::new(HexenHausOdioArchive::new(
87 reader,
88 archive_encoding,
89 config,
90 )?))
91 }
92
93 fn extensions(&self) -> &'static [&'static str] {
94 &["bin"]
95 }
96
97 fn script_type(&self) -> &'static ScriptType {
98 &ScriptType::HexenHausOdio
99 }
100
101 fn is_this_format(&self, _filename: &str, buf: &[u8], buf_len: usize) -> Option<u8> {
102 if buf_len >= ODIO_SIGNATURE.len() && buf.starts_with(ODIO_SIGNATURE) {
103 Some(10)
104 } else {
105 None
106 }
107 }
108
109 fn is_archive(&self) -> bool {
110 true
111 }
112}
113
114#[derive(Debug, Clone)]
115struct HexenHausOdioEntry {
116 name: String,
117 offset: u64,
118 size: u64,
119}
120
121#[derive(Debug)]
122pub struct HexenHausOdioArchive<T: Read + Seek + std::fmt::Debug> {
124 reader: Arc<Mutex<T>>,
125 entries: Vec<HexenHausOdioEntry>,
126}
127
128impl<T: Read + Seek + std::fmt::Debug> HexenHausOdioArchive<T> {
129 pub fn new(mut reader: T, _archive_encoding: Encoding, _config: &ExtraConfig) -> Result<Self> {
131 reader.seek(SeekFrom::Start(0))?;
132 let mut signature = [0u8; 4];
133 reader.read_exact(&mut signature)?;
134 if signature != *ODIO_SIGNATURE {
135 return Err(anyhow!("Invalid HexenHaus ODIO signature"));
136 }
137
138 let reserved = reader.read_u32()?;
139 if reserved != 0 {
140 return Err(anyhow!("Unexpected reserved field in ODIO header"));
141 }
142
143 reader.seek(SeekFrom::Start(HEADER_CHECK_OFFSET))?;
144 let header_check = reader.read_u32()?;
145 if header_check != HEADER_CHECK_VALUE {
146 return Err(anyhow!("Invalid HexenHaus ODIO header check value"));
147 }
148
149 let file_length = reader.seek(SeekFrom::End(0))?;
150 reader.seek(SeekFrom::Start(INDEX_START))?;
151 let first_offset = u64::from(reader.read_u32()?);
152 if first_offset < INDEX_START {
153 return Err(anyhow!("First entry offset precedes index start"));
154 }
155 if first_offset > file_length {
156 return Err(anyhow!("First entry offset exceeds file length"));
157 }
158
159 let index_len = first_offset
160 .checked_sub(INDEX_START)
161 .ok_or_else(|| anyhow!("Invalid index length in ODIO archive"))?;
162 if index_len % INDEX_ENTRY_SIZE != 0 {
163 return Err(anyhow!("ODIO index length is not aligned"));
164 }
165 let entry_count_u64 = index_len / INDEX_ENTRY_SIZE;
166 let entry_count =
167 usize::try_from(entry_count_u64).map_err(|_| anyhow!("ODIO entry count overflow"))?;
168 if entry_count == 0 {
169 return Err(anyhow!("ODIO archive contains no entries"));
170 }
171
172 let mut entries = Vec::with_capacity(entry_count);
173 let mut index_offset = INDEX_START;
174 let mut next_offset = first_offset;
175
176 for i in 0..entry_count {
177 let entry_offset = next_offset;
178
179 index_offset = index_offset
180 .checked_add(INDEX_ENTRY_SIZE)
181 .ok_or_else(|| anyhow!("Index offset overflow"))?;
182
183 if i + 1 == entry_count {
184 next_offset = file_length;
185 } else {
186 if index_offset + 4 > file_length {
187 return Err(anyhow!("Index offset exceeds file length"));
188 }
189 reader.seek(SeekFrom::Start(index_offset))?;
190 next_offset = u64::from(reader.read_u32()?);
191 }
192
193 if entry_offset > file_length {
194 return Err(anyhow!("Entry offset exceeds file length"));
195 }
196 if next_offset > file_length {
197 return Err(anyhow!("Entry extends beyond file length"));
198 }
199 if next_offset < entry_offset {
200 return Err(anyhow!("Entry offsets are out of order"));
201 }
202
203 let size = next_offset - entry_offset;
204 if size == 0 {
205 continue;
206 }
207
208 let name = format!("{:04}.ogg", i);
209 entries.push(HexenHausOdioEntry {
210 name,
211 offset: entry_offset,
212 size,
213 });
214 }
215
216 if entries.is_empty() {
217 return Err(anyhow!("ODIO archive contains no readable entries"));
218 }
219
220 reader.seek(SeekFrom::Start(0))?;
221 Ok(HexenHausOdioArchive {
222 reader: Arc::new(Mutex::new(reader)),
223 entries,
224 })
225 }
226}
227
228impl<T: Read + Seek + std::fmt::Debug + std::any::Any> Script for HexenHausOdioArchive<T> {
229 fn default_output_script_type(&self) -> OutputScriptType {
230 OutputScriptType::Json
231 }
232
233 fn default_format_type(&self) -> FormatOptions {
234 FormatOptions::None
235 }
236
237 fn is_archive(&self) -> bool {
238 true
239 }
240
241 fn iter_archive_filename<'a>(
242 &'a self,
243 ) -> Result<Box<dyn Iterator<Item = Result<String>> + 'a>> {
244 Ok(Box::new(
245 self.entries.iter().map(|entry| Ok(entry.name.clone())),
246 ))
247 }
248
249 fn iter_archive_offset<'a>(&'a self) -> Result<Box<dyn Iterator<Item = Result<u64>> + 'a>> {
250 Ok(Box::new(self.entries.iter().map(|entry| Ok(entry.offset))))
251 }
252
253 fn open_file<'a>(&'a self, index: usize) -> Result<Box<dyn ArchiveContent + 'a>> {
254 if index >= self.entries.len() {
255 return Err(anyhow!(
256 "Index out of bounds: {} (total files: {})",
257 index,
258 self.entries.len()
259 ));
260 }
261 let entry = self.entries[index].clone();
262
263 let decrypt = if entry.size >= ENTRY_HEADER_SIZE {
264 let mut header = [0u8; 4];
265 let mut guard = self
266 .reader
267 .lock()
268 .map_err(|e| anyhow!("Failed to lock reader: {}", e))?;
269 guard.seek(SeekFrom::Start(entry.offset))?;
270 guard.read_exact(&mut header)?;
271 header == *b"ONCE"
272 } else {
273 false
274 };
275
276 let (data_offset, data_size) = if decrypt {
277 let data_offset = entry
278 .offset
279 .checked_add(ENTRY_HEADER_SIZE)
280 .ok_or_else(|| anyhow!("Entry data offset overflow"))?;
281 let data_size = entry
282 .size
283 .checked_sub(ENTRY_HEADER_SIZE)
284 .ok_or_else(|| anyhow!("Entry data size underflow"))?;
285 (data_offset, data_size)
286 } else {
287 (entry.offset, entry.size)
288 };
289
290 Ok(Box::new(OdioEntry {
291 name: entry.name,
292 reader: self.reader.clone(),
293 data_offset,
294 data_size,
295 pos: 0,
296 decrypt,
297 }))
298 }
299}
300
301struct OdioEntry<T: Read + Seek> {
302 name: String,
303 reader: Arc<Mutex<T>>,
304 data_offset: u64,
305 data_size: u64,
306 pos: u64,
307 decrypt: bool,
308}
309
310impl<T: Read + Seek> ArchiveContent for OdioEntry<T> {
311 fn name(&self) -> &str {
312 &self.name
313 }
314}
315
316impl<T: Read + Seek> Read for OdioEntry<T> {
317 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
318 let total_size = self.data_size;
319 if self.pos >= total_size {
320 return Ok(0);
321 }
322
323 let remaining = total_size - self.pos;
324 let remaining_usize = match usize::try_from(remaining) {
325 Ok(value) => value,
326 Err(_) => usize::MAX,
327 };
328 let to_read = remaining_usize.min(buf.len());
329 if to_read == 0 {
330 return Ok(0);
331 }
332
333 let absolute_offset = match self.data_offset.checked_add(self.pos) {
334 Some(offset) => offset,
335 None => {
336 return Err(std::io::Error::new(
337 std::io::ErrorKind::InvalidInput,
338 "Read position overflow",
339 ));
340 }
341 };
342
343 let mut guard = self.reader.lock().map_err(|e| {
344 std::io::Error::new(
345 std::io::ErrorKind::Other,
346 format!("Failed to lock mutex: {}", e),
347 )
348 })?;
349 guard.seek(SeekFrom::Start(absolute_offset))?;
350 let bytes_read = guard.read(&mut buf[..to_read])?;
351 drop(guard);
352
353 if self.decrypt {
354 for byte in &mut buf[..bytes_read] {
355 *byte = byte.rotate_right(4);
356 }
357 }
358
359 self.pos = self.pos.saturating_add(bytes_read as u64);
360 Ok(bytes_read)
361 }
362}
363
364impl<T: Read + Seek> Seek for OdioEntry<T> {
365 fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
366 let new_pos = match pos {
367 SeekFrom::Start(offset) => offset,
368 SeekFrom::End(offset) => {
369 let size = i64::try_from(self.data_size).map_err(|_| {
370 std::io::Error::new(
371 std::io::ErrorKind::InvalidInput,
372 "Data size exceeds seek range",
373 )
374 })?;
375 let target = size.checked_add(offset).ok_or_else(|| {
376 std::io::Error::new(
377 std::io::ErrorKind::InvalidInput,
378 "Seek from end caused overflow",
379 )
380 })?;
381 if target < 0 {
382 return Err(std::io::Error::new(
383 std::io::ErrorKind::InvalidInput,
384 "Seek from end before start",
385 ));
386 }
387 target as u64
388 }
389 SeekFrom::Current(offset) => {
390 let current = i64::try_from(self.pos).map_err(|_| {
391 std::io::Error::new(
392 std::io::ErrorKind::InvalidInput,
393 "Current position overflow",
394 )
395 })?;
396 let target = current.checked_add(offset).ok_or_else(|| {
397 std::io::Error::new(
398 std::io::ErrorKind::InvalidInput,
399 "Seek from current caused overflow",
400 )
401 })?;
402 if target < 0 {
403 return Err(std::io::Error::new(
404 std::io::ErrorKind::InvalidInput,
405 "Seek before start",
406 ));
407 }
408 target as u64
409 }
410 };
411 self.pos = new_pos;
412 Ok(self.pos)
413 }
414
415 fn stream_position(&mut self) -> std::io::Result<u64> {
416 Ok(self.pos)
417 }
418}